/*
 * OREKIT-X
 * Copyright 2002-2008 CS Communication & Systemes
 * 
 * Parts of this software package have been licensed to CS
 * Communication & Systemes (CS) under one or more contributor license
 * agreements.  See the NOTICE file distributed with this work for
 * additional information.
 *  
 * This is an experimental copy of OREKIT from www.orekit.org.
 * Please use the original OREKIT from orekit.org for normal work
 * unrelated to this research project.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You
 * may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package ore.data;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import ore.errors.OrekitException;

/**
 * Class representing a Poisson series for nutation or ephemeris
 * computations.
 * 
 * A Poisson series is composed of a time polynomial part and a
 * non-polynomial part which consist in summation series. The {@link
 * SeriesTerm series terms} are harmonic functions (combination of
 * sines and cosines) of polynomial <em>arguments</em>. The polynomial
 * arguments are combinations of luni-solar or planetary {@link
 * BodiesElements elements}.
 *
 * @author Luc Maisonobe
 * @see SeriesTerm
 */
public class PoissonSeries
    implements java.io.Serializable
{
    /** 
     * Error message for non IERS files.
     */
    private static final String NOT_IERS_FILE =
        "file {0} is not an IERS data file";


    /** 
     * Coefficients of the polynomial part.
     */
    protected final double[] coefficients;
    /** 
     * Non-polynomial series.
     */
    protected final SeriesTerm[][] series;


    /**
     * Subclass constructor for static data.
     */
    protected PoissonSeries(double[] coefficients, SeriesTerm[][] series){
        super();
        this.coefficients = coefficients;
        this.series = series;
    }
    /** 
     * Read a Poisson series from an IERS table file.
     * 
     * @param stream Stream containing the IERS table.  The stream
     * must be closed by the caller.
     * @param factor Multiplicative factor to use for coefficients
     * @param name Name of the resource file (for error messages only)
     * 
     * @exception OrekitException if stream is null or the table cannot be parsed
     */
    public PoissonSeries(InputStream stream, final double factor, String name)
        throws OrekitException
    {
        super();
        try {
            /*
             * The polynomial part should read something like:
             * -16616.99 + 2004191742.88 t - 427219.05 t^2 - 198620.54 t^3 - 46.05 t^4 + 5.98 t^5
             * or something like:
             * 0''.014506 + 4612''.15739966t + 1''.39667721t^2 - 0''.00009344t^3 + 0''.00001882t^4
             */
            final Pattern termPattern =
                Pattern.compile("\\p{Space}*([-+]?)" +
                                "\\p{Space}*(\\p{Digit}+)(?:'')?(\\.\\p{Digit}+)" +
                                "(?:\\p{Space}*t(?:\\^\\p{Digit}+)?)?");
            /*
             * The series parts should read something like:
             * j = 0  Nb of terms = 1306
             *
             *  1    -6844318.44        1328.67    0    0    0    0    1    0    0    0    0    0    0    0    0    0
             *  2     -523908.04        -544.76    0    0    2   -2    2    0    0    0    0    0    0    0    0    0
             *  3      -90552.22         111.23    0    0    2    0    2    0    0    0    0    0    0    0    0    0
             *  4       82168.76         -27.64    0    0    0    0    2    0    0    0    0    0    0    0    0    0
             */
            final Pattern seriesHeaderPattern =
                Pattern.compile("^\\p{Space}*j\\p{Space}*=\\p{Space}*(\\p{Digit}+)" +
                                ".*=\\p{Space}*(\\p{Digit}+)\\p{Space}*$");
            /*
             */
            final BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
            String line = reader.readLine();
            int lineNumber = 1;
            double[] coefficients = null;
            /*
             * Scan input for polynomial coefficients
             */
            while (true) {
                coefficients = ParsePolynomial(termPattern.matcher(line), factor);
                if (null != coefficients)
                    /*
                     * Have parsed the polynomial part
                     */
                    break;

                else {
                    /*
                     * Still in the header
                     */
                    line = reader.readLine();
                    ++lineNumber;
                }
            }
            if (coefficients == null) {
                throw new OrekitException(NOT_IERS_FILE, name);
            }
            else
                this.coefficients = coefficients;

            line = reader.readLine();
            ++lineNumber;

            /*
             * Scan input for the non-polynomial part
             */
            final List<SeriesTerm[]> array = new ArrayList<SeriesTerm[]>();
            while (line != null) {
                final int nTerms = ParseSeriesHeader(seriesHeaderPattern.matcher(line),
                                                     array.size(), name, lineNumber);
                if (nTerms >= 0) {
                    /*
                     * A non-polynomial series
                     */
                    line = reader.readLine();
                    ++lineNumber;

                    while ((line != null) && (line.trim().length() == 0)) {
                        /*
                         * Skip blank lines
                         */
                        line = reader.readLine();
                        ++lineNumber;
                    }
                    /*
                     * Read the terms of the current series
                     */
                    final SeriesTerm[] serie = new SeriesTerm[nTerms];
                    for (int i = 0; i < nTerms; ++i)
                    {
                        serie[i] = ParseSeriesTerm(line, factor, name, lineNumber);
                        line = reader.readLine();
                        ++lineNumber;
                    }
                    /*
                     * Series completed
                     */
                    array.add(serie);
                } 
                else {
                    /*
                     * In the intermediate lines
                     */
                    line = reader.readLine();
                    ++lineNumber;
                }
            }

            if (array.isEmpty()) {
                throw new OrekitException(NOT_IERS_FILE, name);
            }
            else {
                /*
                 * Store the non-polynomial part series
                 */
                this.series = (SeriesTerm[][]) array.toArray(new SeriesTerm[array.size()][]);
            }
        }
        catch (NumberFormatException nfe) {
            throw new OrekitException(nfe.getMessage(), nfe);
        }
        catch (IOException ioe) {
            throw new OrekitException(ioe.getMessage(), ioe);
        }
    }


    /** 
     * Compute the value of the development for the current date.
     * 
     * @param t current date
     * @param elements luni-solar and planetary elements for the current date
     * 
     * @return current value of the development
     */
    public double value(double t, BodiesElements elements) {
        double[] coefficients = this.coefficients;
        /*
         * Polynomial part
         */
        double p = 0;
        for (int i = coefficients.length - 1; i >= 0; --i) {
            p = p * t + coefficients[i];
        }
        /*
         * Non-polynomial part
         */
        SeriesTerm[][] series = this.series;
        double np = 0;
        for (int i = series.length - 1; i >= 0; --i) {

            SeriesTerm[] serie = series[i];
            /* 
             * Add the harmonic terms starting from the last
             * (smallest) terms, to avoid numerical problems
             */
            double s = 0;
            for (int k = serie.length - 1; k >= 0; --k) {
                s += serie[k].value(elements);
            }

            np = np * t + s;
        }
        /*
         * Add the polynomial and the non-polynomial parts
         */
        return (p + np);
    }

    /** 
     * Parse a polynomial description line.
     * @param termMatcher Matcher for the polynomial terms
     * @param factor Multiplicative factor to use for coefficients
     * @return True if the polynomial description line was parsed
     * successfully
     */
    private static double[] ParsePolynomial(final Matcher termMatcher, final double factor) {
        /*
         * Parse the polynomial one polynomial term after the other
         */
        if (!termMatcher.lookingAt())
            return null;

        else {
            /*
             * Store the concatenated sign, integer and fractional
             * parts of the monomial coefficient
             */
            final List<String> coeffs = new ArrayList<String>();
            do {
                coeffs.add(termMatcher.group(1) + termMatcher.group(2) + termMatcher.group(3));
            } 
            while (termMatcher.find());
            /*
             * Parse the coefficients
             */
            int count = coeffs.size();
            double[] coefficients = new double[count];
            for (int i = 0; i < count; ++i) {
                coefficients[i] = factor * Double.parseDouble(coeffs.get(i));
            }
            return coefficients;
        }
    }
    /** 
     * Parse a series header line.
     * 
     * @param headerMatcher matcher for the series header line
     * @param expected expected series index
     * @param name name of the resource file (for error messages only)
     * @param lineNumber line number (for error messages only)
     * 
     * @return the number of terms in the series (-1 if the line
     * cannot be parsed)
     * 
     * @exception OrekitException if the header does not match
     * the expected series number
     */
    private static int ParseSeriesHeader(Matcher headerMatcher, int expected,
                                         String name, int lineNumber)
        throws OrekitException
    {
        /*
         * Is this a series header line ?
         */
        if (!headerMatcher.matches())
            return -1;
        else {
            /*
             * Sanity check
             */
            if (Integer.parseInt(headerMatcher.group(1)) != expected) {
                throw new OrekitException("missing serie j = {0} in file {1} (line {2})",
                                          expected, name, lineNumber);
            }
            else
                return Integer.parseInt(headerMatcher.group(2));
        }
    }
    /** 
     * Parse a series term line.
     * 
     * @param line data line to parse
     * @param factor multiplicative factor to use for coefficients
     * @param name name of the resource file (for error messages only)
     * @param lineNumber line number (for error messages only)
     * 
     * @return A series term
     * 
     * @exception OrekitException if the line is null or cannot be parsed
     */
    private static SeriesTerm ParseSeriesTerm(String line, double factor,
                                              String name, int lineNumber)
        throws OrekitException
    {
        /*
         * Sanity check
         */
        if (line == null) {
            throw new OrekitException("unexpected end of file {0} (after line {1})",
                                      name, lineNumber - 1);
        }
        else {
            /*
             * Parse the Poisson series term
             */
            String[] fields = line.split("\\p{Space}+");
            int l = fields.length;
            if ((l == 17) || ((l == 18) && (fields[0].length() == 0)))
            {
                return SeriesTerm.TermFor(Double.parseDouble(fields[l - 16]) * factor,
                                          Double.parseDouble(fields[l - 15]) * factor,
                                          Integer.parseInt(fields[l - 14]), Integer.parseInt(fields[l - 13]),
                                          Integer.parseInt(fields[l - 12]), Integer.parseInt(fields[l - 11]),
                                          Integer.parseInt(fields[l - 10]), Integer.parseInt(fields[l -  9]),
                                          Integer.parseInt(fields[l -  8]), Integer.parseInt(fields[l -  7]),
                                          Integer.parseInt(fields[l -  6]), Integer.parseInt(fields[l -  5]),
                                          Integer.parseInt(fields[l -  4]), Integer.parseInt(fields[l -  3]),
                                          Integer.parseInt(fields[l -  2]), Integer.parseInt(fields[l -  1]));
            }
            else
                throw new OrekitException("unable to parse line {0} of file {1}:\n{2}",
                                          lineNumber, name, line);
        }
    }

}
